// HTMLParser Library $Name: v1_6 $ - A java-based parser for HTML
// http://sourceforge.org/projects/htmlparser
// Copyright (C) 2003 Derrick Oswald
//
// Revision Control Information
//
// $Source: /cvsroot/htmlparser/htmlparser/src/org/htmlparser/lexerapplications/thumbelina/Thumbelina.java,v $
// $Author: derrickoswald $
// $Date: 2005/02/13 20:36:00 $
// $Revision: 1.7 $
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//

package org.htmlparser.lexerapplications.thumbelina;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Image;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.ImageObserver;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;

import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JSplitPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.border.BevelBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.htmlparser.Node;
import org.htmlparser.Tag;
import org.htmlparser.lexer.Lexer;
import org.htmlparser.util.ParserException;

/**
 * View images behind thumbnails.
 */
public class Thumbelina extends JPanel
		// was: java.awt.Canvas
		implements Runnable, ItemListener, ChangeListener,
		ListSelectionListener {
	/**
	 * Property name for current URL binding.
	 */
	public static final String PROP_CURRENT_URL_PROPERTY = "currentURL";
	/**
	 * Property name for queue size binding.
	 */
	public static final String PROP_URL_QUEUE_PROPERTY = "queueSize";
	/**
	 * Property name for visited URL size binding.
	 */
	public static final String PROP_URL_VISITED_PROPERTY = "visitedSize";

	/**
	 * URL's to visit.
	 */
	private ArrayList mUrls;

	/**
	 * URL's visited.
	 */
	protected HashMap mVisited;

	/**
	 * Images requested.
	 */
	protected HashMap mRequested;

	/**
	 * Images being tracked currently.
	 */
	protected HashMap mTracked;

	/**
	 * Background thread.
	 */
	protected Thread mThread;

	/**
	 * Activity state. <code>true</code> means processing URLS,
	 * <code>false</code> not.
	 */
	protected boolean mActive;

	/**
	 * The picture sequencer.
	 */
	protected Sequencer mSequencer;

	/**
	 * The central area for pictures.
	 */
	protected PicturePanel mPicturePanel;

	/**
	 * Value returned when no links are discovered.
	 */
	protected static final URL[][] NONE = { {}, {} };

	/**
	 * Bound property support.
	 */
	protected PropertyChangeSupport mPropertySupport;

	/**
	 * The URL being currently being examined.
	 */
	protected String mCurrentURL;

	/**
	 * If <code>true</code>, does not follow links containing cgi calls.
	 */
	protected boolean mDiscardCGI;

	/**
	 * If <code>true</code>, does not follow links containing queries (?).
	 */
	protected boolean mDiscardQueries;

	/**
	 * Background thread checkbox in status bar.
	 */
	protected JCheckBox mBackgroundToggle;

	/**
	 * History list.
	 */
	protected JList mHistory;

	/**
	 * Scroller for the picture panel.
	 */
	protected JScrollPane mPicturePanelScroller;

	/**
	 * Scroller for the history list.
	 */
	protected JScrollPane mHistoryScroller;

	/**
	 * Main panel in central area.
	 */
	protected JSplitPane mMainArea;

	/**
	 * Status bar.
	 */
	protected JPanel mPowerBar;

	/**
	 * Image request queue monitor in status bar.
	 */
	protected JProgressBar mQueueProgress;

	/**
	 * Image ready queue monitor in status bar.
	 */
	protected JProgressBar mReadyProgress;

	/**
	 * Sequencer thread toggle in status bar.
	 */
	protected JCheckBox mRunToggle;

	/**
	 * Sequencer speed slider in status bar.
	 */
	protected JSlider mSpeedSlider;

	/**
	 * URL report in status bar.
	 */
	protected JTextField mUrlText;

	/**
	 * URL queue size display in status bar.
	 */
	protected JLabel mQueueSize;

	/**
	 * URL visited count display in status bar.
	 */
	protected JLabel mVisitedSize;

	/**
	 * Creates a new instance of Thumbelina.
	 */
	public Thumbelina() {
		this((URL) null);
	}

	/**
	 * Creates a new instance of Thumbelina.
	 * 
	 * @param url
	 *            Single URL to enter into the 'to follow' list.
	 * @exception MalformedURLException
	 *                If the url is malformed.
	 */
	public Thumbelina(final String url) throws MalformedURLException {
		this(null == url ? null : new URL(url));
	}

	/**
	 * Creates a new instance of Thumbelina.
	 * 
	 * @param url
	 *            URL to enter into the 'to follow' list.
	 */
	public Thumbelina(final URL url) {
		mUrls = new ArrayList();
		mVisited = new HashMap();
		mRequested = new HashMap();
		mTracked = new HashMap();
		mThread = null;
		mActive = true;
		mPicturePanel = new PicturePanel(this);
		mSequencer = new Sequencer(this);
		mPropertySupport = new PropertyChangeSupport(this);
		mCurrentURL = null;
		mDiscardCGI = true;
		mDiscardQueries = true;

		// JComponent specific
		setDoubleBuffered(false);
		setLayout(new java.awt.BorderLayout());
		mPicturePanel.setDoubleBuffered(false);

		mThread = new Thread(this);
		mThread.setName("BackgroundThread");
		mThread.start();
		initComponents();

		mRunToggle.addItemListener(this);
		mBackgroundToggle.addItemListener(this);
		mSpeedSlider.addChangeListener(this);
		mHistory.addListSelectionListener(this);

		memCheck();

		if (null != url)
			append(url);
	}

	/**
	 * Check for low memory situation. Report to the user a bad situation.
	 */
	protected void memCheck() {
		Runtime runtime;
		long maximum;

		if (System.getProperty("java.version").startsWith("1.4")) {
			runtime = Runtime.getRuntime();
			runtime.gc();
			maximum = runtime.maxMemory();
			if (maximum < 67108864L) // 64MB
				JOptionPane
						.showMessageDialog(
								null,
								"Maximum available memory is low ("
										+ maximum
										+ " bytes).\n"
										+ "\n"
										+ "It is strongly suggested to increase the maximum memory\n"
										+ "available by using the JVM command line switch -Xmx with\n"
										+ "a suitable value, such as -Xmx256M for example.",
								"Thumbelina - Low memory",
								JOptionPane.WARNING_MESSAGE, null /* Icon */);
		}
	}

	/**
	 * Reset this Thumbelina. Clears the sequencer of pending images, resets the
	 * picture panel, emptiies the 'to be examined' list of URLs.
	 */
	public void reset() {
		int oldsize;

		synchronized (mUrls) {
			mSequencer.reset();
			mPicturePanel.reset();
			oldsize = mUrls.size();
			mUrls.clear();
		}
		updateQueueSize(oldsize, mUrls.size());
	}

	/**
	 * Append the given URL to the queue. Adds the url only if it isn't already
	 * in the queue, and notifys listeners about the addition.
	 * 
	 * @param url
	 *            The url to add.
	 */
	public void append(final URL url) {
		String href;
		boolean found;
		URL u;
		int oldsize;

		href = url.toExternalForm();
		found = false;
		oldsize = -1;
		synchronized (mUrls) {
			for (int i = 0; !found && (i < mUrls.size()); i++) {
				u = (URL) mUrls.get(i);
				if (href.equals(u.toExternalForm()))
					found = true;
			}
			if (!found) {
				oldsize = mUrls.size();
				mUrls.add(url);
				mUrls.notify();
			}
		}
		if (-1 != oldsize)
			updateQueueSize(oldsize, mUrls.size());
	}

	/**
	 * Append the given URLs to the queue.
	 * 
	 * @param list
	 *            The list of URL objects to add.
	 */
	public void append(final ArrayList list) {
		for (int i = 0; i < list.size(); i++)
			append((URL) list.get(i));
	}

	/**
	 * Filter URLs and add to queue. Removes already visited links and appends
	 * the rest (if any) to the visit pending list.
	 * 
	 * @param urls
	 *            The list of URL's to add to the 'to visit' list.
	 * @return Returns the filered list.
	 */
	protected ArrayList filter(final URL[] urls) {
		ArrayList list;
		URL url;
		String ref;

		list = new ArrayList();
		for (int i = 0; i < urls.length; i++) {
			url = urls[i];
			ref = url.toExternalForm();
			// ignore cgi
			if (!mDiscardCGI || (-1 == ref.indexOf("/cgi-bin/")))
				// ignore queries
				if (!mDiscardQueries || (-1 == ref.indexOf("?")))
					// ignore duplicates
					if (!mVisited.containsKey(ref)) {
						try {
							url.openConnection();
							list.add(url);
						} catch (IOException ioe) {
							// unknown host or some other problem... discard
						}
					}
		}

		return (list);
	}

	/**
	 * Initialize the GUI.
	 */
	private void initComponents() {
		mPowerBar = new JPanel();
		mUrlText = new JTextField();
		mRunToggle = new JCheckBox();
		mSpeedSlider = new JSlider();
		mReadyProgress = new JProgressBar();
		mQueueProgress = new JProgressBar();
		mBackgroundToggle = new JCheckBox();
		mMainArea = new JSplitPane();
		mPicturePanelScroller = new JScrollPane();
		mHistoryScroller = new JScrollPane();
		mHistory = new JList();
		mQueueSize = new JLabel();
		mVisitedSize = new JLabel();

		mPowerBar.setLayout(new BoxLayout(mPowerBar, BoxLayout.X_AXIS));

		mPowerBar.setBorder(new BevelBorder(BevelBorder.LOWERED));
		mPowerBar.add(mUrlText);

		mRunToggle.setSelected(true);
		mRunToggle.setText("On/Off");
		mRunToggle.setToolTipText("Starts/stops the image presentation.");
		mPowerBar.add(mRunToggle);

		mSpeedSlider.setMajorTickSpacing(1000);
		mSpeedSlider.setMaximum(5000);
		mSpeedSlider.setPaintTicks(true);
		mSpeedSlider.setToolTipText("Set inter-image delay.");
		mSpeedSlider.setValue(500);
		mSpeedSlider.setInverted(true);
		mPowerBar.add(mSpeedSlider);

		mReadyProgress.setToolTipText("Pending images..");
		mReadyProgress.setStringPainted(true);
		mPowerBar.add(mReadyProgress);

		mQueueProgress.setToolTipText("Outstanding image fetches..");
		mQueueProgress.setStringPainted(true);
		mPowerBar.add(mQueueProgress);

		mBackgroundToggle.setSelected(true);
		mBackgroundToggle.setText("On/Off");
		mBackgroundToggle.setToolTipText("Starts/stops background fetching.");
		mPowerBar.add(mBackgroundToggle);

		mVisitedSize.setBorder(new BevelBorder(BevelBorder.LOWERED));
		mVisitedSize.setText("00000");
		mVisitedSize.setToolTipText("Number of URLs examined.");
		mPowerBar.add(mVisitedSize);
		mQueueSize.setBorder(new BevelBorder(BevelBorder.LOWERED));
		mQueueSize.setText("00000");
		mQueueSize.setToolTipText("Number of URLs in queue.");
		mPowerBar.add(mQueueSize);

		mHistory.setModel(new DefaultListModel());
		mHistory.setToolTipText("History");
		mHistory.setDoubleBuffered(false);
		mHistory
				.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
		mHistoryScroller.setViewportView(mHistory);
		mHistoryScroller.setDoubleBuffered(false);
		mPicturePanelScroller.setViewportView(mPicturePanel);
		mPicturePanelScroller.setDoubleBuffered(false);
		mPicturePanelScroller
				.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
		mPicturePanelScroller
				.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);

		add(mMainArea, java.awt.BorderLayout.CENTER);
		mMainArea.setLeftComponent(mHistoryScroller);
		mMainArea.setRightComponent(mPicturePanelScroller);
		add(mPowerBar, java.awt.BorderLayout.SOUTH);
	}

	/**
	 * Gets the state of status bar visibility.
	 * 
	 * @return <code>true</code> if the status bar is visible.
	 */
	public boolean getStatusBarVisible() {
		boolean ret;

		ret = false;

		for (int i = 0; !ret && (i < getComponentCount()); i++)
			if (mPowerBar == getComponent(i))
				ret = true;

		return (ret);
	}

	/**
	 * Sets the status bar visibility.
	 * 
	 * @param visible
	 *            The new visibility state. If <code>true</code>, the status
	 *            bar will be unhidden.
	 */
	public void setStatusBarVisible(final boolean visible) {
		int index;

		index = -1;
		for (int i = 0; (-1 == index) && (i < getComponentCount()); i++)
			if (mPowerBar == getComponent(i))
				index = i;
		if (visible) {
			if (-1 == index) {
				add(mPowerBar, java.awt.BorderLayout.SOUTH);
				invalidate();
				validate();
			}
		} else if (-1 != index) {
			remove(index);
			invalidate();
			validate();
		}
	}

	/**
	 * Gets the state of history list visibility.
	 * 
	 * @return <code>true</code> if the history list is visible.
	 */
	public boolean getHistoryListVisible() {
		boolean ret;

		ret = false;

		for (int i = 0; !ret && (i < getComponentCount()); i++)
			// check indirectly because the history list is in a splitter
			if (mMainArea == getComponent(i))
				ret = true;

		return (ret);
	}

	/**
	 * Sets the history list visibility.
	 * 
	 * @param visible
	 *            The new visibility state. If <code>true</code>, the history
	 *            list will be unhidden.
	 */
	public void setHistoryListVisible(final boolean visible) {
		int pictpanel;
		int splitter;
		Component component;

		pictpanel = -1;
		splitter = -1;
		for (int i = 0; i < getComponentCount(); i++) {
			component = getComponent(i);
			if (mPicturePanelScroller == component)
				pictpanel = i;
			else if (mMainArea == component)
				splitter = i;
		}
		if (visible) {
			if (-1 != pictpanel) {
				remove(pictpanel);
				add(mMainArea, java.awt.BorderLayout.CENTER);
				mMainArea.setLeftComponent(mHistoryScroller);
				// mPicturePanelScroller.setViewportView (mPicturePanel);
				mMainArea.setRightComponent(mPicturePanelScroller);
				invalidate();
				validate();
			}
		} else if (-1 != splitter) {
			remove(splitter);
			add(mPicturePanelScroller, java.awt.BorderLayout.CENTER);
			invalidate();
			validate();
		}
	}

	/**
	 * Gets the state of the sequencer thread.
	 * 
	 * @return <code>true</code> if the thread is pumping images.
	 */
	public boolean getSequencerActive() {
		return (mSequencer.mActive);
	}

	/**
	 * Sets the sequencer activity state. The sequencer is the thread that moves
	 * images from the pending list to the picture panel on a timed basis.
	 * 
	 * @param active
	 *            The new activity state. If <code>true</code>, the sequencer
	 *            will be turned on. This may alter the speed setting if it is
	 *            set to zero.
	 */
	public void setSequencerActive(final boolean active) {
		// check the delay is not zero
		if (0 == getSpeed())
			setSpeed(Sequencer.DEFAULT_DELAY);
		mSequencer.mActive = active;
		if (active)
			synchronized (mSequencer.mPending) {
				mSequencer.mPending.notify();
			}
		if (active != mRunToggle.isSelected())
			mRunToggle.setSelected(active);
	}

	/**
	 * Gets the state of the background thread.
	 * 
	 * @return <code>true</code> if the thread is examining web pages.
	 */
	public boolean getBackgroundThreadActive() {
		return (mActive);
	}

	/**
	 * Sets the state of the background thread activity. The background thread
	 * is responsible for examining URLs that are on the queue for thumbnails,
	 * and starting the image fetch operation.
	 * 
	 * @param active
	 *            If <code>true</code>, the background thread will be turned
	 *            on.
	 */
	public void setBackgroundThreadActive(final boolean active) {
		mActive = active;
		if (active)
			synchronized (mUrls) {
				mUrls.notify();
			}
		if (active != mBackgroundToggle.isSelected())
			mBackgroundToggle.setSelected(active);
	}

	/**
	 * Get the sequencer delay time.
	 * 
	 * @return The number of milliseconds between image additions to the panel.
	 */
	public int getSpeed() {
		return (mSequencer.getDelay());
	}

	/**
	 * Set the sequencer delay time. The sequencer is the thread that moves
	 * images from the pending list to the picture panel on a timed basis. This
	 * value sets the number of milliseconds it waits between pictures. Setting
	 * it to zero toggles the running state off.
	 * 
	 * @param speed
	 *            The sequencer delay in milliseconds.
	 */
	public void setSpeed(final int speed) {
		if (0 == speed)
			mRunToggle.setSelected(false);
		else {
			mRunToggle.setSelected(true);
			mSequencer.setDelay(speed);
		}
		if (speed != mSpeedSlider.getValue())
			mSpeedSlider.setValue(speed);
	}

	/**
	 * Getter for property discardCGI.
	 * 
	 * @return Value of property discardCGI.
	 * 
	 */
	public boolean isDiscardCGI() {
		return (mDiscardCGI);
	}

	/**
	 * Setter for property discardCGI.
	 * 
	 * @param discard
	 *            New value of property discardCGI.
	 * 
	 */
	public void setDiscardCGI(final boolean discard) {
		mDiscardCGI = discard;
	}

	/**
	 * Getter for property discardQueries.
	 * 
	 * @return Value of property discardQueries.
	 * 
	 */
	public boolean isDiscardQueries() {
		return (mDiscardQueries);
	}

	/**
	 * Setter for property discardQueries.
	 * 
	 * @param discard
	 *            New value of property discardQueries.
	 * 
	 */
	public void setDiscardQueries(final boolean discard) {
		mDiscardQueries = discard;
	}

	/**
	 * Check if the url looks like an image.
	 * 
	 * @param url
	 *            The usrl to check for image characteristics.
	 * @return <code>true</code> if the url ends in a recognized image
	 *         extension.
	 */
	protected boolean isImage(final String url) {
		String lower = url.toLowerCase();
		return (lower.endsWith(".jpg") || lower.endsWith(".gif") || lower
				.endsWith(".png"));
	}

	/**
	 * Get the links of an element of a document. Only gets the links on IMG
	 * elements that reference another image. The latter is based on suffix
	 * (.jpg, .gif and .png).
	 * 
	 * @param lexer
	 *            The fully conditioned lexer, ready to rock.
	 * @param docbase
	 *            The url to read.
	 * @return The URLs, targets of the IMG links;
	 * @exception IOException
	 *                If the underlying infrastructure throws it.
	 * @exception ParserException
	 *                If there is a problem parsing the url.
	 */
	protected URL[][] extractImageLinks(final Lexer lexer, final URL docbase)
			throws IOException, ParserException {
		HashMap images;
		HashMap links;
		boolean ina; // true when within a <A></A> pair
		Node node;
		Tag tag;
		String name;
		Tag startatag;
		Tag imgtag;
		String href;
		String src;
		URL url;
		URL[][] ret;

		images = new HashMap();
		links = new HashMap();
		ina = false;
		startatag = null;
		imgtag = null;
		while (null != (node = lexer.nextNode())) {
			if (node instanceof Tag) {
				tag = (Tag) node;
				name = tag.getTagName();
				if ("A".equals(name)) {
					if (tag.isEndTag()) {
						ina = false;
						if (null != imgtag) {
							// evidence of a thumb
							href = startatag.getAttribute("HREF");
							if (null != href) {
								if (isImage(href)) {
									src = imgtag.getAttribute("SRC");
									if (null != src)
										try {
											url = new URL(docbase, href);
											// eliminate duplicates
											href = url.toExternalForm();
											if (!images.containsKey(href))
												images.put(href, url);
										} catch (MalformedURLException murle) {
											// oops, forget it
										}
								}
							}
						}
					} else {
						startatag = tag;
						imgtag = null;
						ina = true;
						href = startatag.getAttribute("HREF");
						if (null != href) {
							if (!isImage(href))
								try {
									url = new URL(docbase, href);
									// eliminate duplicates
									href = url.toExternalForm();
									if (!links.containsKey(href))
										links.put(href, url);
								} catch (MalformedURLException murle) {
									// well, obviously we don't want this one
								}
						}
					}
				} else if (ina && "IMG".equals(name))
					imgtag = tag;
			}
		}

		ret = new URL[2][];
		ret[0] = new URL[images.size()];
		images.values().toArray(ret[0]);
		ret[1] = new URL[links.size()];
		links.values().toArray(ret[1]);

		return (ret);
	}

	/**
	 * Get the image links from the current URL.
	 * 
	 * @param url
	 *            The URL to get the links from
	 * @return An array of two URL arrays, index 0 is a list of images, index 1
	 *         is a list of links to possibly follow.
	 */
	protected URL[][] getImageLinks(final URL url) {
		Lexer lexer;
		URL[][] ret;

		if (null != url) {
			try {
				lexer = new Lexer(url.openConnection());
				ret = extractImageLinks(lexer, url);
			} catch (Throwable t) {
				System.out.println(t.getMessage());
				ret = NONE;
			}
		} else
			ret = NONE;

		return (ret);
	}

	/**
	 * Get the picture panel object encapsulated by this Thumbelina.
	 * 
	 * @return The picture panel.
	 */
	public PicturePanel getPicturePanel() {
		return (mPicturePanel);
	}

	/**
	 * Add a PropertyChangeListener to the listener list. The listener is
	 * registered for all properties.
	 * 
	 * @param listener
	 *            The PropertyChangeListener to be added.
	 */
	public void addPropertyChangeListener(final PropertyChangeListener listener) {
		mPropertySupport.addPropertyChangeListener(listener);
	}

	/**
	 * Remove a PropertyChangeListener from the listener list. This removes a
	 * PropertyChangeListener that was registered for all properties.
	 * 
	 * @param listener
	 *            The PropertyChangeListener to be removed.
	 */
	public void removePropertyChangeListener(
			final PropertyChangeListener listener) {
		mPropertySupport.removePropertyChangeListener(listener);
	}

	/**
	 * Return the URL currently being examined. This is a bound property.
	 * Notifications are available via the PROP_CURRENT_URL_PROPERTY property.
	 * 
	 * @return The size of the 'to be examined' list.
	 */
	public String getCurrentURL() {
		return (mCurrentURL);
	}

	/**
	 * Set the current URL being examined.
	 * 
	 * @param url
	 *            The url that is being examined.
	 */
	protected void setCurrentURL(final String url) {
		String oldValue;

		if (((null != url) && !url.equals(mCurrentURL))
				|| ((null == url) && (null != mCurrentURL))) {
			oldValue = mCurrentURL;
			mCurrentURL = url;
			mPropertySupport.firePropertyChange(PROP_CURRENT_URL_PROPERTY,
					oldValue, url);
		}
	}

	/**
	 * Apply a change in 'to be examined' URL list size. Sends notification via
	 * the <code>PROP_URL_QUEUE_PROPERTY</code> property and updates the
	 * status bar.
	 * 
	 * @param original
	 *            The original size of the list.
	 * @param current
	 *            The new size of the list.
	 */
	protected void updateQueueSize(final int original, final int current) {
		StringBuffer buffer;

		buffer = new StringBuffer();
		buffer.append(current);
		while (buffer.length() < 5)
			buffer.insert(0, '0');
		mQueueSize.setText(buffer.toString());
		mPropertySupport.firePropertyChange(PROP_URL_QUEUE_PROPERTY, original,
				current);
	}

	/**
	 * Apply a change in 'visited' URL list size. Sends notification via the
	 * <code>PROP_URL_VISITED_PROPERTY</code> property and updates the status
	 * bar.
	 * 
	 * @param original
	 *            The original size of the list.
	 * @param current
	 *            The new size of the list.
	 */
	protected void updateVisitedSize(final int original, final int current) {
		StringBuffer buffer;

		buffer = new StringBuffer();
		buffer.append(current);
		while (buffer.length() < 5)
			buffer.insert(0, '0');
		mVisitedSize.setText(buffer.toString());
		mPropertySupport.firePropertyChange(PROP_URL_VISITED_PROPERTY,
				original, current);
	}

	/**
	 * Fetch images. Ask the toolkit to make the image from a URL, and add a
	 * tracker to handle it when it's received. Add details to the rquested and
	 * tracked lists and update the status bar.
	 * 
	 * @param images
	 *            The list of images to fetch.
	 */
	protected void fetch(final URL[] images) {
		Image image;
		Tracker tracker;
		int size;

		for (int j = 0; j < images.length; j++) {
			if (!mRequested.containsKey(images[j].toExternalForm())) {
				image = getToolkit().createImage(images[j]);
				tracker = new Tracker(images[j]);
				synchronized (mTracked) {
					size = mTracked.size() + 1;
					if (mQueueProgress.getMaximum() < size) {
						try {
							mTracked.wait();
						} catch (InterruptedException ie) {
							// this won't happen, just continue on
						}
					}
					mRequested.put(images[j].toExternalForm(), images[j]);
					mTracked.put(images[j].toExternalForm(), images[j]);
					mQueueProgress.setValue(size);
					image.getWidth(tracker); // trigger the observer
				}
			}
		}
	}

	//
	// Runnable interface
	//

	/**
	 * The main processing loop. Pull suspect URLs off the queue one at a time,
	 * fetch and parse it, request images and enqueue further links.
	 */
	public void run() {
		URL link;
		int original;
		String href;
		URL[][] urls;

		while (true) {
			try {
				link = null;
				original = -1;
				synchronized (mUrls) {
					if (0 != mUrls.size()) {
						original = mUrls.size();
						link = (URL) mUrls.remove(0);
					} else
						// don't spin crazily on an empty list
						Thread.sleep(100);
				}
				if (null != link) {
					updateQueueSize(original, mUrls.size());
					href = link.toExternalForm();
					setCurrentURL(href);
					mVisited.put(href, link);
					updateVisitedSize(mVisited.size() - 1, mVisited.size());
					urls = getImageLinks(link);
					fetch(urls[0]);
					// append (filter (urls[1]));
					synchronized (mEnqueuers) {
						Enqueuer enqueuer = new Enqueuer(urls[1]);
						enqueuer.setPriority(Thread.MIN_PRIORITY);
						mEnqueuers.add(enqueuer);
						enqueuer.start();
					}
					setCurrentURL(null);
				}
				if (!mActive)
					synchronized (mUrls) {
						mUrls.wait();
					}
			} catch (Throwable t) {
				t.printStackTrace();
			}
		}
	}

	static ArrayList mEnqueuers = new ArrayList();

	class Enqueuer extends Thread {
		URL[] mList;

		public Enqueuer(URL[] list) {
			mList = list;
		}

		public void run() {
			append(filter(mList));
			synchronized (mEnqueuers) {
				mEnqueuers.remove(this);
			}
		}
	}

	//
	// ItemListener interface
	//

	/**
	 * Handle checkbox events from the status bar. Based on the thread toggles,
	 * activates or deactivates the background thread processes.
	 * 
	 * @param event
	 *            The event describing the checkbox event.
	 */
	public void itemStateChanged(final ItemEvent event) {
		Object source;
		boolean checked;

		source = event.getItemSelectable();
		checked = ItemEvent.SELECTED == event.getStateChange();
		if (source == mRunToggle)
			setSequencerActive(checked);
		else if (source == mBackgroundToggle)
			setBackgroundThreadActive(checked);
	}

	//
	// ChangeListener interface
	//

	/**
	 * Handles the speed slider events.
	 * 
	 * @param event
	 *            The event describing the slider activity.
	 */
	public void stateChanged(final ChangeEvent event) {
		JSlider source;

		source = (JSlider) event.getSource();
		if (!source.getValueIsAdjusting())
			setSpeed(source.getValue());
	}

	//
	// ListSelectionListener interface
	//

	/**
	 * Handles the history list events.
	 * 
	 * @param event
	 *            The event describing the list activity.
	 */
	public void valueChanged(final ListSelectionEvent event) {
		JList source;
		Object[] hrefs;
		Picture picture;
		URL url;
		Image image;
		Tracker tracker;

		source = (JList) event.getSource();
		if (source == mHistory && !event.getValueIsAdjusting()) {
			hrefs = source.getSelectedValues();
			for (int i = 0; i < hrefs.length; i++) {
				picture = mPicturePanel.find("http://" + (String) hrefs[i]);
				if (null != picture)
					mPicturePanel.bringToTop(picture);
				else
					try {
						url = new URL("http://" + (String) hrefs[i]);
						image = getToolkit().createImage(url);
						tracker = new Tracker(url);
						image.getWidth(tracker);
						System.out.println("refetching " + hrefs[i]);
					} catch (MalformedURLException murle) {
						murle.printStackTrace();
					}
			}
		}
	}

	/**
	 * Adds the given url to the history list. Also puts the URL in the url text
	 * of the status bar.
	 * 
	 * @param url
	 *            The URL to add to the history list.
	 */
	public void addHistory(String url) {
		int index;
		DefaultListModel model;

		mUrlText.setText(url);
		// chop off the protocol
		index = url.indexOf("http://");
		if (-1 != index)
			url = url.substring(index + 7);
		else
			System.out.println("********* " + url + " ************");
		model = (DefaultListModel) mHistory.getModel();
		model.addElement(url);
		// this doesn't friggin work:
		// mHistory.ensureIndexIsVisible (model.getSize ());
	}

	/**
	 * Open a URL. Resets the urls list and appends the given url as the only
	 * item.
	 * 
	 * @param ref
	 *            The URL to add.
	 */
	public void open(String ref) {
		URL url;

		try {
			if (!ref.startsWith("http://"))
				ref = "http://" + ref;
			url = new URL(ref);
			reset();
			append(url);
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}

	/**
	 * Provide command line help.
	 */
	protected static void help() {
		System.out
				.println("Thumbelina - Scan and display the images behind thumbnails.");
		System.out.println("java -Xmx256M -jar thumbelina.jar [url]");
		System.out.println("It is highly recommended that the maximum heap "
				+ "size be increased with -Xmx switch.");
		System.exit(0);
	}

	/**
	 * Mainline.
	 * 
	 * @param args
	 *            the command line arguments. Can be one or more forms of -help
	 *            to get command line help, or a URL to prime the program with.
	 *            Checks for JDK 1.4 and if not found runs in crippled mode (no
	 *            ThumbelinaFrame).
	 */
	public static void main(final String[] args) {
		URL url;
		String version;
		JFrame frame;
		Thumbelina thumbelina;

		System.setProperty("sun.net.client.defaultReadTimeout", "7000");
		System.setProperty("sun.net.client.defaultConnectTimeout", "7000");

		url = null;
		if (0 != args.length)
			try {
				if (args[0].equalsIgnoreCase("help")
						|| args[0].equalsIgnoreCase("-help")
						|| args[0].equalsIgnoreCase("-h")
						|| args[0].equalsIgnoreCase("?")
						|| args[0].equalsIgnoreCase("-?"))
					help();
				else
					url = new URL(args[0]);
			} catch (MalformedURLException murle) {
				System.err.println(murle.getMessage());
				help();
			}

		version = System.getProperty("java.version");
		if (version.startsWith("1.4") || version.startsWith("1.5"))
			frame = new ThumbelinaFrame(url);
		else {
			if (null == url)
				help();
			System.out.println("Java version is only " + version
					+ ", entering crippled mode");
			frame = new JFrame("Thumbelina");
			thumbelina = new Thumbelina(url);
			frame.getContentPane().add(thumbelina, BorderLayout.CENTER);
			frame.setBounds(10, 10, 640, 480);
			frame.addWindowListener(new WindowAdapter() {
				public void windowClosing(final WindowEvent event) {
					System.exit(0);
				}
			});
		}
		frame.setVisible(true);
	}

	/**
	 * Getter for property queue.
	 * 
	 * @return List of URLs that are to be visited.
	 */
	public ArrayList getQueue() {
		return (mUrls);
	}

	/**
	 * Getter for property queue. This is a bound property. Notifications are
	 * available via the <code>PROP_URL_QUEUE_PROPERTY</code> property.
	 * 
	 * @return The size of the list of URLs that are to be visited.
	 */
	public int getQueueSize() {
		return (mUrls.size());
	}

	/**
	 * Track incoming asynchronous image reception. On completion, adds the
	 * image to the pending list.
	 */
	class Tracker implements ImageObserver {

		/**
		 * The url the image is comming from.
		 */
		protected URL mSource;

		/**
		 * Create an image tracker.
		 * 
		 * @param source
		 *            The URL the image is being fetched from.
		 */
		public Tracker(final URL source) {
			mSource = source;
		}

		//
		// ImageObserver interface
		//

		/**
		 * This method is called when information about an image which was
		 * previously requested using an asynchronous interface becomes
		 * available. Asynchronous interfaces are method calls such as
		 * getWidth(ImageObserver) and drawImage(img, x, y, ImageObserver) which
		 * take an ImageObserver object as an argument. Those methods register
		 * the caller as interested either in information about the overall
		 * image itself (in the case of getWidth(ImageObserver)) or about an
		 * output version of an image (in the case of the drawImage(img, x, y,
		 * [w, h,] ImageObserver) call).
		 * 
		 * <p>
		 * This method should return true if further updates are needed or false
		 * if the required information has been acquired. The image which was
		 * being tracked is passed in using the img argument. Various constants
		 * are combined to form the infoflags argument which indicates what
		 * information about the image is now available. The interpretation of
		 * the x, y, width, and height arguments depends on the contents of the
		 * infoflags argument.
		 * <p>
		 * The <code>infoflags</code> argument should be the bitwise inclusive
		 * <b>OR</b> of the following flags: <code>WIDTH</code>,
		 * <code>HEIGHT</code>, <code>PROPERTIES</code>,
		 * <code>SOMEBITS</code>, <code>FRAMEBITS</code>,
		 * <code>ALLBITS</code>, <code>ERROR</code>, <code>ABORT</code>.
		 * 
		 * @param image
		 *            the image being observed.
		 * @param infoflags
		 *            the bitwise inclusive OR of the following flags:
		 *            <code>WIDTH</code>, <code>HEIGHT</code>,
		 *            <code>PROPERTIES</code>, <code>SOMEBITS</code>,
		 *            <code>FRAMEBITS</code>, <code>ALLBITS</code>,
		 *            <code>ERROR</code>, <code>ABORT</code>.
		 * @param x
		 *            the <i>x</i> coordinate.
		 * @param y
		 *            the <i>y</i> coordinate.
		 * @param width
		 *            the width.
		 * @param height
		 *            the height.
		 * @return <code>false</code> if the infoflags indicate that the image
		 *         is completely loaded; <code>true</code> otherwise.
		 * 
		 * @see #WIDTH
		 * @see #HEIGHT
		 * @see #PROPERTIES
		 * @see #SOMEBITS
		 * @see #FRAMEBITS
		 * @see #ALLBITS
		 * @see #ERROR
		 * @see #ABORT
		 * @see Image#getWidth
		 * @see Image#getHeight
		 * @see java.awt.Graphics#drawImage
		 */
		public synchronized boolean imageUpdate(final Image image,
				final int infoflags, final int x, final int y, final int width,
				final int height) {
			boolean done;
			boolean error;
			boolean abort;
			URL url;

			done = (0 != (infoflags & ImageObserver.ALLBITS));
			abort = (0 != (infoflags & ImageObserver.ABORT));
			error = (0 != (infoflags & ImageObserver.ERROR));
			if (done || abort || error)
				synchronized (mTracked) {
					url = (URL) mTracked.remove(mSource.toExternalForm());
					mTracked.notify();
					mQueueProgress.setValue(mTracked.size());
					if (done)
						mSequencer.add(image, mSource, (null != url));
				}

			return (!done);
		}
	}
}

/*
 * Revision Control Modification History
 * 
 * $Log: Thumbelina.java,v $ Revision 1.7 2005/02/13 20:36:00 derrickoswald
 * FilterBuilder
 * 
 * Revision 1.6 2004/07/31 16:42:30 derrickoswald Remove unused variables and
 * other fixes exposed by turning on compiler warnings.
 * 
 * Revision 1.5 2004/05/24 16:18:17 derrickoswald Part three of a multiphase
 * refactoring. The three node types are now fronted by interfaces (program to
 * the interface paradigm) with concrete implementations in the new
 * htmlparser.nodes package. Classes from the lexer.nodes package are moved to
 * this package, and obvious references to the concrete classes that got broken
 * by this have been changed to use the interfaces where possible.
 * 
 * Revision 1.4 2004/05/16 17:59:56 derrickoswald Alter bound property name
 * constants to agree with section 8.8 Capitalization of inferred names. in the
 * JavaBeans API specification.
 * 
 * Revision 1.3 2003/11/04 01:25:02 derrickoswald Made visiting order the same
 * order as on the page. The 'shouldRecurseSelf' boolean of NodeVisitor could
 * probably be removed since it doesn't make much sense any more. Fixed
 * StringBean, which was still looking for end tags with names starting with a
 * slash, i.e. "/SCRIPT", silly beany. Added some debugging support to the
 * lexer, you can easily base a breakpoint on line number.
 * 
 * Revision 1.2 2003/10/26 16:44:01 derrickoswald Get thumbelina working again.
 * The tag.getName() method doesn't include the / of end tags.
 * 
 * Revision 1.1 2003/09/21 18:20:56 derrickoswald Thumbelina Created a lexer GUI
 * application to extract images behind thumbnails. Added a task in the ant
 * build script - thumbelina - to create the jar file. You need JDK 1.4.x to
 * build it. It can be run on JDK 1.3.x in crippled mode. Usage: java -Xmx256M
 * thumbelina.jar [URL]
 * 
 * 
 */
